実装方式

FFT を素直に実装すると計算量の導出に書いたような処理になる。 ただしこれらのコードは並べ替えのために単純にコピーを行ったり同じ $\omega$ の計算を繰り返し行ったりするなど効率がよくない。 そのため一般的にはそのような無駄を取り除いた方式の実装を FFT の実装として考えることが多い。 ここでは広く知られている 2 種類の実装方式を紹介する。

プログラムを実装する際にどちらの方が良いかは細かな実装や ユーザの需要次第となるので決定的な言及はできないが、 大まかには小メモリ動作を目指すなら In-place 型を、 速さを求めるなら Self-sorting 型を使用する印象である。

様々に提案されている FFT の各種高速化手法については基本的に 実装方法がどちらの方式であっても適用することができる。 In-place 型 FFT での効率化実装については[JW06]が詳しいので、 本サイトでは Self-sorting 型 FFT を用いて説明している。

Stockham 型 FFT / self-sorting 型 FFT

冒頭部分で述べたような、再帰呼び出しや同じ $\omega$ の計算を素直に省いた実装を Self-sorting 型 FFT、もしくは開発者の名をとって Stockham 型 FFT という。 Self-sorting の名の通りデータが常に並んでいるため計算がしやすい。 特徴としてデータ整列のために元のデータと同程度のデータ領域を必要とする。 領域 A → 領域 B → 領域 A →… と計算結果を交互に置いていくので Ping-pong FFT とも言われる。

時間間引き FFT

void SelfSortDITCore(int n, int m, Complex* x, Complex* y) {
  const int l = n / m / 2;
  for (int k = 0; k < m; ++k) {
    double wt = -M_PI * k / m;
    Complex w = Complex::polar(0, wt);
    for (int j = 0; j < l; ++j) {
      int xi0 = j * m + k, xi1 = xi0 + n / 2;
      int yi0 = j * 2 * m + k, yi1 = yi0 + m;
      Complex wx1 = x[xi1] * w;
      y[yi1] = x[xi0] - wx0;
      y[yi0] = x[xi0] + wx0;
    }
  }
}

void SelfSortDIT(int n, Complex* x, Complex* y) {
  for (int m = 1, i = 0; m < n; m *= 2, ++i) {
    if (i % 2) {
      SelfSortDITCore(n, m, y, x);
    } else {
      SelfSortDITCore(n, m, x, (m == n / 2) ? x : y);
    }
  }
}

周波数間引き FFT

void SelfSortDIFCore(int n, int m, Complex* x, Complex* y) {
  const int l = n / m / 2;
  for (int k = 0; k < m; ++k) {
    double wt = -M_PI * k / m;
    Complex w = Complex::polar(0, wt);
    for (int j = 0; j < l; ++j) {
      int xi0 = j * 2 * m + k, xi1 = xi0 + m;
      int yi0 = j * m + k, yi1 = yi0 + n / 2;
      Complex t0 = x[xi0] + x[xi1];
      Complex t1 = (x[xi0] - x[xi1]) * w;
      y[yi0] = t0;
      y[yi1] = t1;
    }
  }
}

void SelfSortDIF(int n, Complex* x, Complex* y) {
  int m = n / 2;
  if (static_cast<int>(log2(n)) % 2) {
    SelfSortDIFCore(n, m, x, x);
    m /= 2;
  }
  for (int i = 0; m >= 1; m /= 2, ++i) {
    if (i % 2) {
      SelfSortDIFCore(n, m, y, x);
    } else {
      SelfSortDIFCore(n, m, x, y);
    }
  }
}

Cooley-Tukey 型 FFT / in-place 型 FFT

計算量の導出に書かれたコードを見直すと、 計算の前後 (DIT では前、DIF では後ろ) にデータの並べ替えのみを行っている 部分があることが分かる。 これは事前にデータの移動先、もしくは元のデータ位置が分かっていれば 並べ替えの処理、ならびに並べ替えのデータ領域が必要ない事を示していて、 実際に (2 基底の FFT では) ビット逆順 (bit reversal order) と呼ばれる順序に並び替えることで実現することができる。 この方式に基づいてメモリ領域の無駄を省いた実装を in-place 型 FFT、 もしくは Cooley-Tukey 型 FFT という。 一般にこの方式では計算前後にデータの並べ替え処理を行う必要があるが、 畳み込み乗算への応用のみを考える場合は 並べ替えない方針を取ることもできる。

以下に bit reverse の順序に並べ替えるコードを書いておく。

void BitReverse(int n, Complex* x) {
  for (int i = 1, j = 0; i < n; ++i) {
    for (int b = n >> 1; b; b >>= 1) {
      j ^= b;
      if (j & b) break;
    }
    if (i < j)
      swap(x[i], x[j]);
  }
}

時間間引き FFT

void InPlaceDIT(int n, Complex* x) {
  BitReverse(n, x);
  for (int m = 1; m < n; m *= 2) {
    for (int k = 0; k < m; ++k) {
      double wt = -M_PI * k / m;
      Complex w = Complex::polar(0, wt);
      for (int j = 0; j < n; j += 2 * m) {
        Complex &x0 = x[k + j], &x1 = x[k + j + m];
        Complex x1w = x1 * w;
        x1 = x0 - x1w;
        x0 = x0 + x1w;
      }
    }
  }
}

周波数間引き FFT

void InPlaceDIF(int n, Complex* x) {
  for (int m = n / 2; m >= 1; m /= 2) {
    for (int k = 0; k < m; ++k) {
      Complex w = Complex::polar(0, wt);
      for (int j = 0; j < n; j += 2 * m) {
        Complex &x0 = x[k + j], &x1 = x[k + j + m];
        Complex x1w = (x0 - x1) * w;
        x0 = x0 + x1;
        x1 = x1w;
      }
    }
  }
  BitReverse(n, x);
}